// Copyright 2016-present the Material Components for iOS authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#import "MDCCollectionViewController.h"

#import "private/MDCCollectionInfoBarView.h"
#import "private/MDCCollectionStringResources.h"
#import "private/MDCCollectionViewEditor.h"
#import "private/MDCCollectionViewStyler.h"
#import "MaterialCollectionCells.h"
#import "MDCCollectionViewEditing.h"
#import "MDCCollectionViewEditingDelegate.h"
#import "MDCCollectionViewFlowLayout.h"
#import "MDCCollectionInfoBarViewDelegate.h"

#include <tgmath.h>

NSString *const MDCCollectionInfoBarKindHeader = @"MDCCollectionInfoBarKindHeader";
NSString *const MDCCollectionInfoBarKindFooter = @"MDCCollectionInfoBarKindFooter";

@interface MDCCollectionViewController () <MDCCollectionInfoBarViewDelegate,
                                           MDCInkTouchControllerDelegate,
                                           MDCRippleTouchControllerDelegate>
@property(nonatomic, assign) BOOL currentlyActiveInk;
@end

@implementation MDCCollectionViewController {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
  MDCInkTouchController *_inkTouchController;
#pragma clang diagnostic pop
  MDCRippleTouchController *_rippleTouchController;
  MDCCollectionInfoBarView *_headerInfoBar;
  MDCCollectionInfoBarView *_footerInfoBar;
  BOOL _headerInfoBarDismissed;
  CGPoint _inkTouchLocation;
}

@synthesize collectionViewLayout = _collectionViewLayout;

- (instancetype)init {
  MDCCollectionViewFlowLayout *defaultLayout = [[MDCCollectionViewFlowLayout alloc] init];
  return [self initWithCollectionViewLayout:defaultLayout];
}

- (instancetype)initWithCollectionViewLayout:(UICollectionViewLayout *)layout {
  self = [super initWithCollectionViewLayout:layout];
  if (self) {
    _collectionViewLayout = layout;
  }
  return self;
}

- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil
                         bundle:(nullable NSBundle *)nibBundleOrNil {
  self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
  if (self != nil) {
    // TODO(#): Why is this nil, the decoder should have created it
    if (!_collectionViewLayout) {
      _collectionViewLayout = [[MDCCollectionViewFlowLayout alloc] init];
    }
  }

  return self;
}

- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
  self = [super initWithCoder:aDecoder];
  if (self != nil) {
    // TODO(#): Why is this nil, the decoder should have created it
    if (!_collectionViewLayout) {
      _collectionViewLayout = [[MDCCollectionViewFlowLayout alloc] init];
    }
  }

  return self;
}

- (void)viewDidLoad {
  [super viewDidLoad];

  if (@available(iOS 11.0, *)) {
    self.collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAlways;
  }

  [self.collectionView setCollectionViewLayout:self.collectionViewLayout];
  self.collectionView.backgroundColor = [UIColor whiteColor];
  self.collectionView.alwaysBounceVertical = YES;

  _styler = [[MDCCollectionViewStyler alloc] initWithCollectionView:self.collectionView];
  _styler.delegate = self;

  _editor = [[MDCCollectionViewEditor alloc] initWithCollectionView:self.collectionView];
  _editor.delegate = self;

  // Set up ink touch controller.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
  _inkTouchController = [[MDCInkTouchController alloc] initWithView:self.collectionView];
#pragma clang diagnostic pop
  _inkTouchController.delegate = self;

  _rippleTouchController = [[MDCRippleTouchController alloc] initWithView:self.collectionView
                                                                 deferred:YES];
  _rippleTouchController.delegate = self;

  // Register our supplementary header and footer
  NSString *classIdentifier = NSStringFromClass([MDCCollectionInfoBarView class]);
  NSString *headerKind = MDCCollectionInfoBarKindHeader;
  NSString *footerKind = MDCCollectionInfoBarKindFooter;
  [self.collectionView registerClass:[MDCCollectionInfoBarView class]
          forSupplementaryViewOfKind:headerKind
                 withReuseIdentifier:classIdentifier];
  [self.collectionView registerClass:[MDCCollectionInfoBarView class]
          forSupplementaryViewOfKind:footerKind
                 withReuseIdentifier:classIdentifier];
}

- (void)viewDidLayoutSubviews {
  [super viewDidLayoutSubviews];

  // Fixes an iOS 11 bug where supplementary views would be given a zPosition of 1, meaning the
  // scroll view indicator (with a zPosition of 0) would be placed behind supplementary views.
  // We know that iOS keeps the scroll indicator as the top-most view in the hierarchy as a subview,
  // so we grab it and give it a better zPosition ourselves.
  if (@available(iOS 11.0, *)) {
    UIView *maybeScrollViewIndicator = self.collectionView.subviews.lastObject;
    if ([maybeScrollViewIndicator isKindOfClass:[UIImageView class]]) {
      maybeScrollViewIndicator.layer.zPosition = 2;
    }
  }
}

- (void)setCollectionView:(__kindof UICollectionView *)collectionView {
  [super setCollectionView:collectionView];

  // Reset editor and ink to provided collection view.
  _editor = [[MDCCollectionViewEditor alloc] initWithCollectionView:collectionView];
  _editor.delegate = self;
  if (self.enableRippleBehavior) {
    _rippleTouchController = [[MDCRippleTouchController alloc] initWithView:collectionView
                                                                   deferred:YES];
    _rippleTouchController.delegate = self;
  } else {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
    _inkTouchController = [[MDCInkTouchController alloc] initWithView:collectionView];
#pragma clang diagnostic pop
    _inkTouchController.delegate = self;
  }
}

- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
  [super traitCollectionDidChange:previousTraitCollection];

  [self.collectionViewLayout invalidateLayout];
}

#pragma mark - <MDCCollectionInfoBarViewDelegate>

- (void)updateControllerWithInfoBar:(MDCCollectionInfoBarView *)infoBar {
  // Updates info bar styling for header/footer.
  if ([infoBar.kind isEqualToString:MDCCollectionInfoBarKindHeader]) {
    _headerInfoBar = infoBar;
    _headerInfoBar.message = MDCCollectionStringResources(infoBarGestureHintString);
    _headerInfoBar.style = MDCCollectionInfoBarViewStyleHUD;
    [self updateHeaderInfoBarIfNecessary];
  } else if ([infoBar.kind isEqualToString:MDCCollectionInfoBarKindFooter]) {
    _footerInfoBar = infoBar;
    _footerInfoBar.message = MDCCollectionStringResources(deleteButtonString);
    _footerInfoBar.style = MDCCollectionInfoBarViewStyleActionable;
    [self updateFooterInfoBarIfNecessary];
  }
}

- (void)didTapInfoBar:(MDCCollectionInfoBarView *)infoBar {
  if ([infoBar isEqual:_footerInfoBar]) {
    [self deleteIndexPaths:self.collectionView.indexPathsForSelectedItems];
  }
}

- (void)infoBar:(MDCCollectionInfoBarView *)infoBar
    willShowAnimated:(__unused BOOL)animated
     willAutoDismiss:(__unused BOOL)willAutoDismiss {
  if ([infoBar.kind isEqualToString:MDCCollectionInfoBarKindFooter]) {
    [self updateContentWithBottomInset:MDCCollectionInfoBarFooterHeight];
  }
}

- (void)infoBar:(MDCCollectionInfoBarView *)infoBar
    willDismissAnimated:(__unused BOOL)animated
        willAutoDismiss:(BOOL)willAutoDismiss {
  if ([infoBar.kind isEqualToString:MDCCollectionInfoBarKindHeader]) {
    _headerInfoBarDismissed = willAutoDismiss;
  } else {
    [self updateContentWithBottomInset:-MDCCollectionInfoBarFooterHeight];
  }
}

#pragma mark - <MDCCollectionViewStylingDelegate>

- (MDCCollectionViewCellStyle)collectionView:(__unused UICollectionView *)collectionView
                         cellStyleForSection:(__unused NSInteger)section {
  return _styler.cellStyle;
}

#pragma mark - <UICollectionViewDelegateFlowLayout>

- (CGSize)collectionView:(UICollectionView *)collectionView
                    layout:(UICollectionViewLayout *)collectionViewLayout
    sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
  UICollectionViewLayoutAttributes *attr =
      [collectionViewLayout layoutAttributesForItemAtIndexPath:indexPath];
  CGSize size = [self sizeWithAttribute:attr collectionView:collectionView];
  size = [self inlaidSizeAtIndexPath:indexPath withSize:size collectionView:collectionView];
  return size;
}

- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView
                        layout:(__unused UICollectionViewLayout *)collectionViewLayout
        insetForSectionAtIndex:(NSInteger)section {
  return [self insetsAtSectionIndex:section collectionView:collectionView];
}

- (CGFloat)collectionView:(__unused UICollectionView *)collectionView
                                 layout:(UICollectionViewLayout *)collectionViewLayout
    minimumLineSpacingForSectionAtIndex:(__unused NSInteger)section {
  if ([collectionViewLayout isKindOfClass:[UICollectionViewFlowLayout class]]) {
    if (_styler.cellLayoutType == MDCCollectionViewCellLayoutTypeGrid) {
      return _styler.gridPadding;
    }
    return [(UICollectionViewFlowLayout *)collectionViewLayout minimumLineSpacing];
  }
  return 0;
}

- (CGSize)sizeWithAttribute:(UICollectionViewLayoutAttributes *)attr
             collectionView:(UICollectionView *)collectionView {
  CGFloat height = MDCCellDefaultOneLineHeight;
  if ([_styler.delegate respondsToSelector:@selector(collectionView:cellHeightAtIndexPath:)]) {
    height = [_styler.delegate collectionView:collectionView cellHeightAtIndexPath:attr.indexPath];
  }

  CGFloat width = [self cellWidthAtSectionIndex:attr.indexPath.section
                                 collectionView:collectionView];
  return CGSizeMake(width, height);
}

// Note that this method is only exposed temporarily until self-sizing cells are supported.
- (CGFloat)cellWidthAtSectionIndex:(NSInteger)section
                    collectionView:(UICollectionView *)collectionView {
  UIEdgeInsets contentInset = collectionView.contentInset;
  // On the iPhone X, we need to use the offset which might take into account the safe area.
  if (@available(iOS 11.0, *)) {
    contentInset = collectionView.adjustedContentInset;
  }

  CGFloat bounds = CGRectGetWidth(UIEdgeInsetsInsetRect(collectionView.bounds, contentInset));
  UIEdgeInsets sectionInsets = [self collectionView:collectionView
                                             layout:collectionView.collectionViewLayout
                             insetForSectionAtIndex:section];
  CGFloat insets = sectionInsets.left + sectionInsets.right;
  if (_styler.cellLayoutType == MDCCollectionViewCellLayoutTypeGrid) {
    CGFloat cellWidth = bounds - insets - (_styler.gridPadding * (_styler.gridColumnCount - 1));
    return cellWidth / _styler.gridColumnCount;
  }
  return bounds - insets;
}

- (UIEdgeInsets)insetsAtSectionIndex:(NSInteger)section
                      collectionView:(UICollectionView *)collectionView {
  // Determine insets based on cell style.
  CGFloat inset = (CGFloat)floor(MDCCollectionViewCellStyleCardSectionInset);
  UIEdgeInsets insets = UIEdgeInsetsZero;
  NSInteger numberOfSections = collectionView.numberOfSections;
  BOOL isTop = (section == 0);
  BOOL isBottom = (section == numberOfSections - 1);
  MDCCollectionViewCellStyle cellStyle = [_styler cellStyleAtSectionIndex:section];
  BOOL isCardStyle = cellStyle == MDCCollectionViewCellStyleCard;
  BOOL isGroupedStyle = cellStyle == MDCCollectionViewCellStyleGrouped;
  // Set left/right insets.
  if (isCardStyle) {
    insets.left = inset;
    insets.right = inset;
    if (@available(iOS 11.0, *)) {
      if (collectionView.contentInsetAdjustmentBehavior ==
          UIScrollViewContentInsetAdjustmentAlways) {
        // We don't need section insets if there are already safe area insets.
        insets.left = MAX(0, insets.left - collectionView.safeAreaInsets.left);
        insets.right = MAX(0, insets.right - collectionView.safeAreaInsets.right);
      }
    }
  }
  // Set top/bottom insets.
  if (isCardStyle || isGroupedStyle) {
    insets.top = (CGFloat)floor((isTop) ? inset : inset / 2);
    insets.bottom = (CGFloat)floor((isBottom) ? inset : inset / 2);
  }
  return insets;
}

- (CGSize)inlaidSizeAtIndexPath:(NSIndexPath *)indexPath
                       withSize:(CGSize)size
                 collectionView:(UICollectionView *)collectionView {
  // If object is inlaid, return its adjusted size.
  if ([_styler isItemInlaidAtIndexPath:indexPath]) {
    CGFloat inset = MDCCollectionViewCellStyleCardSectionInset;
    UIEdgeInsets inlayInsets = UIEdgeInsetsZero;
    BOOL prevCellIsInlaid = NO;
    BOOL nextCellIsInlaid = NO;

    BOOL hasSectionHeader = NO;
    if ([self respondsToSelector:@selector(collectionView:
                                                   layout:referenceSizeForHeaderInSection:)]) {
      CGSize headerSize = [self collectionView:collectionView
                                        layout:collectionView.collectionViewLayout
               referenceSizeForHeaderInSection:indexPath.section];
      hasSectionHeader = !CGSizeEqualToSize(headerSize, CGSizeZero);
    }

    BOOL hasSectionFooter = NO;
    if ([self respondsToSelector:@selector(collectionView:
                                                   layout:referenceSizeForFooterInSection:)]) {
      CGSize footerSize = [self collectionView:collectionView
                                        layout:_collectionViewLayout
               referenceSizeForFooterInSection:indexPath.section];
      hasSectionFooter = !CGSizeEqualToSize(footerSize, CGSizeZero);
    }

    // Check if previous cell is inlaid.
    if (indexPath.item > 0 || hasSectionHeader) {
      NSIndexPath *prevIndexPath = [NSIndexPath indexPathForItem:(indexPath.item - 1)
                                                       inSection:indexPath.section];
      prevCellIsInlaid = [_styler isItemInlaidAtIndexPath:prevIndexPath];
      inlayInsets.top = prevCellIsInlaid ? inset / 2 : inset;
    }

    // Check if next cell is inlaid.
    if (indexPath.item < [collectionView numberOfItemsInSection:indexPath.section] - 1 ||
        hasSectionFooter) {
      NSIndexPath *nextIndexPath = [NSIndexPath indexPathForItem:(indexPath.item + 1)
                                                       inSection:indexPath.section];
      nextCellIsInlaid = [_styler isItemInlaidAtIndexPath:nextIndexPath];
      inlayInsets.bottom = nextCellIsInlaid ? inset / 2 : inset;
    }

    // Apply top/bottom height adjustments to inlaid object.
    size.height += inlayInsets.top + inlayInsets.bottom;
  }
  return size;
}

#pragma mark - Subclassing Methods

/*
 The below method is solely used for subclasses to retrieve width information in order to
 calculate cell height. Not meant to call method cellWidthAtSectionIndex:collectionView as
 that method recalculates section insets which we don't want to do.
 */
- (CGFloat)cellWidthAtSectionIndex:(NSInteger)section {
  UIEdgeInsets contentInset = self.collectionView.contentInset;
  // On the iPhone X, we need to use the offset which might take into account the safe area.
  if (@available(iOS 11.0, *)) {
    contentInset = self.collectionView.adjustedContentInset;
  }
  CGFloat bounds = CGRectGetWidth(UIEdgeInsetsInsetRect(self.collectionView.bounds, contentInset));
  UIEdgeInsets sectionInsets = [self collectionView:self.collectionView
                                             layout:self.collectionView.collectionViewLayout
                             insetForSectionAtIndex:section];

  CGFloat insets = sectionInsets.left + sectionInsets.right;
  if (_styler != nil) {
    if (_styler.cellLayoutType == MDCCollectionViewCellLayoutTypeGrid) {
      CGFloat cellWidth = bounds - insets - (_styler.gridPadding * (_styler.gridColumnCount - 1));
      if (_styler.gridColumnCount > 0) {
        return cellWidth / _styler.gridColumnCount;
      }
    }
  }
  return bounds - insets;
}

#pragma mark - <MDCInkTouchControllerDelegate>
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
- (BOOL)inkTouchController:(__unused MDCInkTouchController *)inkTouchController
    shouldProcessInkTouchesAtTouchLocation:(CGPoint)location {
  // Only store touch location and do not allow ink processing. This ink location will be used when
  // manually starting/stopping the ink animation during cell highlight/unhighlight states.
  if (!self.currentlyActiveInk) {
    _inkTouchLocation = location;
  }
  return NO;
}

- (MDCInkView *)inkTouchController:(MDCInkTouchController *)inkTouchController
            inkViewAtTouchLocation:(CGPoint)location {
  NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:location];
  UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPath];
  MDCInkView *ink = nil;

  if ([_styler.delegate respondsToSelector:@selector(collectionView:
                                                 inkTouchController:inkViewAtIndexPath:)]) {
    return [_styler.delegate collectionView:self.collectionView
                         inkTouchController:inkTouchController
                         inkViewAtIndexPath:indexPath];
  }
  if ([cell isKindOfClass:[MDCCollectionViewCell class]]) {
    MDCCollectionViewCell *inkCell = (MDCCollectionViewCell *)cell;
    if (!inkCell.enableRippleBehavior) {
      if ([inkCell respondsToSelector:@selector(inkView)]) {
        // Set cell ink.
        ink = [cell performSelector:@selector(inkView)];
      }
    }
  }

  return ink;
}
#pragma clang diagnostic pop

#pragma mark - <MDCRippleTouchControllerDelegate>

- (BOOL)rippleTouchController:(MDCRippleTouchController *)rippleTouchController
    shouldProcessRippleTouchesAtTouchLocation:(CGPoint)location {
  // Only store touch location and do not allow ripple processing. This ripple location will be used
  // when manually starting/stopping the ripple animation during cell highlight/unhighlight states.
  if (!self.currentlyActiveInk) {
    _inkTouchLocation = location;
  }
  return NO;
}

- (MDCRippleView *)rippleTouchController:(MDCRippleTouchController *)rippleTouchController
               rippleViewAtTouchLocation:(CGPoint)location {
  NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:location];
  UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPath];
  MDCRippleView *ripple = nil;

  if ([_styler.delegate respondsToSelector:@selector(collectionView:
                                               rippleTouchController:rippleViewAtIndexPath:)]) {
    return [_styler.delegate collectionView:self.collectionView
                      rippleTouchController:rippleTouchController
                      rippleViewAtIndexPath:indexPath];
  }
  if ([cell isKindOfClass:[MDCCollectionViewCell class]]) {
    MDCCollectionViewCell *rippleCell = (MDCCollectionViewCell *)cell;
    if (rippleCell.enableRippleBehavior) {
      if ([rippleCell respondsToSelector:@selector(rippleView)]) {
        // Set cell ripple.
        ripple = [cell performSelector:@selector(rippleView)];
      }
    }
  }

  return ripple;
}

#pragma mark - <UICollectionViewDataSource>

- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView
           viewForSupplementaryElementOfKind:(NSString *)kind
                                 atIndexPath:(NSIndexPath *)indexPath {
  // TODO (shepj): This implementation of registering cell classes in data source methods should be
  // rethought. This causes a crash without a workaround when collections with headers or
  // footers entering editing mode. Also, we should find a way around implementing a data source
  // method in a super class.
  // Issue: https://github.com/material-components/material-components-ios/issues/1208
  // Editing info bar.
  if ([kind isEqualToString:MDCCollectionInfoBarKindHeader] ||
      [kind isEqualToString:MDCCollectionInfoBarKindFooter]) {
    NSString *identifier = NSStringFromClass([MDCCollectionInfoBarView class]);
    UICollectionReusableView *supplementaryView =
        [collectionView dequeueReusableSupplementaryViewOfKind:kind
                                           withReuseIdentifier:identifier
                                                  forIndexPath:indexPath];

    // Update info bar.
    if ([supplementaryView isKindOfClass:[MDCCollectionInfoBarView class]]) {
      MDCCollectionInfoBarView *infoBarView = (MDCCollectionInfoBarView *)supplementaryView;
      infoBarView.delegate = self;
      infoBarView.kind = kind;
      [self updateControllerWithInfoBar:infoBarView];
    }
    return supplementaryView;
  } else {
    return [super collectionView:collectionView
        viewForSupplementaryElementOfKind:kind
                              atIndexPath:indexPath];
  }
}

#pragma mark - <UICollectionViewDelegate>

- (BOOL)collectionView:(__unused UICollectionView *)collectionView
    shouldHighlightItemAtIndexPath:(__unused NSIndexPath *)indexPath {
  return YES;
}

- (void)collectionView:(UICollectionView *)collectionView
    didHighlightItemAtIndexPath:(NSIndexPath *)indexPath {
  if ([_styler.delegate respondsToSelector:@selector(collectionView:hidesInkViewAtIndexPath:)] &&
      [_styler.delegate collectionView:collectionView hidesInkViewAtIndexPath:indexPath]) {
    return;
  }
  UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
  CGPoint location = [collectionView convertPoint:_inkTouchLocation toView:cell];

  if (self.enableRippleBehavior) {
    MDCRippleView *rippleView;
    if ([cell respondsToSelector:@selector(rippleView)]) {
      rippleView = [cell performSelector:@selector(rippleView)];
    } else {
      return;
    }

    if ([_styler.delegate respondsToSelector:@selector(collectionView:inkColorAtIndexPath:)]) {
      rippleView.rippleColor = [_styler.delegate collectionView:collectionView
                                            inkColorAtIndexPath:indexPath];
      if (!rippleView.rippleColor) {
        rippleView.rippleColor = [UIColor colorWithWhite:0 alpha:0.12f];
      }
    }
    self.currentlyActiveInk = YES;
    [rippleView beginRippleTouchDownAtPoint:location animated:YES completion:nil];
  } else {
    // Start cell ink show animation.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
    MDCInkView *inkView;
#pragma clang diagnostic pop
    if ([cell respondsToSelector:@selector(inkView)]) {
      inkView = [cell performSelector:@selector(inkView)];
    } else {
      return;
    }

    // Update ink color if necessary.
    if ([_styler.delegate respondsToSelector:@selector(collectionView:inkColorAtIndexPath:)]) {
      inkView.inkColor = [_styler.delegate collectionView:collectionView
                                      inkColorAtIndexPath:indexPath];
      if (!inkView.inkColor) {
        inkView.inkColor = inkView.defaultInkColor;
      }
    }
    self.currentlyActiveInk = YES;
    [inkView startTouchBeganAnimationAtPoint:location completion:nil];
  }
}

- (void)collectionView:(UICollectionView *)collectionView
    didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath {
  UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
  CGPoint location = [collectionView convertPoint:_inkTouchLocation toView:cell];

  if (self.enableRippleBehavior) {
    MDCRippleView *rippleView;
    if ([cell respondsToSelector:@selector(rippleView)]) {
      rippleView = [cell performSelector:@selector(rippleView)];
    } else {
      return;
    }

    self.currentlyActiveInk = NO;
    [rippleView beginRippleTouchUpAnimated:YES completion:nil];
  } else {
    // Start cell ink evaporate animation.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
    MDCInkView *inkView;
#pragma clang diagnostic pop
    if ([cell respondsToSelector:@selector(inkView)]) {
      inkView = [cell performSelector:@selector(inkView)];
    } else {
      return;
    }

    self.currentlyActiveInk = NO;
    [inkView startTouchEndedAnimationAtPoint:location completion:nil];
  }
}

- (BOOL)collectionView:(UICollectionView *)collectionView
    shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath {
  if (_editor.isEditing) {
    if ([self collectionView:collectionView canEditItemAtIndexPath:indexPath]) {
      return [self collectionView:collectionView canSelectItemDuringEditingAtIndexPath:indexPath];
    }
    return NO;
  }
  return YES;
}

- (BOOL)collectionView:(UICollectionView *)collectionView
    shouldDeselectItemAtIndexPath:(__unused NSIndexPath *)indexPath {
  return collectionView.allowsMultipleSelection;
}

- (void)collectionView:(__unused UICollectionView *)collectionView
    didSelectItemAtIndexPath:(__unused NSIndexPath *)indexPath {
  [self updateFooterInfoBarIfNecessary];
}

- (void)collectionView:(__unused UICollectionView *)collectionView
    didDeselectItemAtIndexPath:(__unused NSIndexPath *)indexPath {
  [self updateFooterInfoBarIfNecessary];
}

#pragma mark - <MDCCollectionViewEditingDelegate>

- (BOOL)collectionViewAllowsEditing:(__unused UICollectionView *)collectionView {
  return NO;
}

- (void)collectionViewWillBeginEditing:(__unused UICollectionView *)collectionView {
  if (self.currentlyActiveInk) {
    if (self.enableRippleBehavior) {
      MDCRippleView *activeRippleView = [self rippleTouchController:_rippleTouchController
                                          rippleViewAtTouchLocation:_inkTouchLocation];
      [activeRippleView beginRippleTouchUpAnimated:YES completion:nil];
    } else {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
      MDCInkView *activeInkView = [self inkTouchController:_inkTouchController
                                    inkViewAtTouchLocation:_inkTouchLocation];
#pragma clang diagnostic pop
      [activeInkView startTouchEndedAnimationAtPoint:_inkTouchLocation completion:nil];
    }
  }
  // Inlay all items.
  _styler.allowsItemInlay = YES;
  _styler.allowsMultipleItemInlays = YES;
  [_styler applyInlayToAllItemsAnimated:YES];
  [self updateHeaderInfoBarIfNecessary];
}

- (void)collectionViewWillEndEditing:(__unused UICollectionView *)collectionView {
  // Remove inlay of all items.
  [_styler removeInlayFromAllItemsAnimated:YES];
  [self updateFooterInfoBarIfNecessary];
}

- (BOOL)collectionView:(UICollectionView *)collectionView
    canEditItemAtIndexPath:(__unused NSIndexPath *)indexPath {
  return [self collectionViewAllowsEditing:collectionView];
}

- (BOOL)collectionView:(UICollectionView *)collectionView
    canSelectItemDuringEditingAtIndexPath:(NSIndexPath *)indexPath {
  if ([self collectionViewAllowsEditing:collectionView]) {
    return [self collectionView:collectionView canEditItemAtIndexPath:indexPath];
  }
  return NO;
}

#pragma mark - Item Moving

- (BOOL)collectionViewAllowsReordering:(__unused UICollectionView *)collectionView {
  return NO;
}

- (BOOL)collectionView:(UICollectionView *)collectionView
    canMoveItemAtIndexPath:(__unused NSIndexPath *)indexPath {
  return ([self collectionViewAllowsEditing:collectionView] &&
          [self collectionViewAllowsReordering:collectionView]);
}

- (BOOL)collectionView:(UICollectionView *)collectionView
    canMoveItemAtIndexPath:(NSIndexPath *)indexPath
               toIndexPath:(NSIndexPath *)newIndexPath {
  // First ensure both source and target items can be moved.
  return ([self collectionView:collectionView canMoveItemAtIndexPath:indexPath] &&
          [self collectionView:collectionView canMoveItemAtIndexPath:newIndexPath]);
}

- (void)collectionView:(UICollectionView *)collectionView
    didMoveItemAtIndexPath:(NSIndexPath *)indexPath
               toIndexPath:(NSIndexPath *)newIndexPath {
  [collectionView moveItemAtIndexPath:indexPath toIndexPath:newIndexPath];
}

#pragma mark - Swipe-To-Dismiss-Items

- (BOOL)collectionViewAllowsSwipeToDismissItem:(__unused UICollectionView *)collectionView {
  return NO;
}

- (BOOL)collectionView:(UICollectionView *)collectionView
    canSwipeToDismissItemAtIndexPath:(__unused NSIndexPath *)indexPath {
  return [self collectionViewAllowsSwipeToDismissItem:collectionView];
}

- (void)collectionView:(__unused UICollectionView *)collectionView
    didEndSwipeToDismissItemAtIndexPath:(NSIndexPath *)indexPath {
  [self deleteIndexPaths:@[ indexPath ]];
}

#pragma mark - Swipe-To-Dismiss-Sections

- (BOOL)collectionViewAllowsSwipeToDismissSection:(__unused UICollectionView *)collectionView {
  return NO;
}

- (BOOL)collectionView:(UICollectionView *)collectionView
    canSwipeToDismissSection:(__unused NSInteger)section {
  return [self collectionViewAllowsSwipeToDismissSection:collectionView];
}

- (void)collectionView:(__unused UICollectionView *)collectionView
    didEndSwipeToDismissSection:(NSInteger)section {
  [self deleteSections:[NSIndexSet indexSetWithIndex:section]];
}

#pragma mark - Private

- (void)deleteIndexPaths:(NSArray<NSIndexPath *> *)indexPaths {
  if ([self respondsToSelector:@selector(collectionView:willDeleteItemsAtIndexPaths:)]) {
    void (^batchUpdates)(void) = ^{
      // Notify delegate to delete data.
      [self collectionView:self.collectionView willDeleteItemsAtIndexPaths:indexPaths];

      // Delete index paths.
      [self.collectionView deleteItemsAtIndexPaths:indexPaths];
    };

    void (^completionBlock)(BOOL finished) = ^(__unused BOOL finished) {
      [self updateFooterInfoBarIfNecessary];
      // Notify delegate of deletion.
      if ([self respondsToSelector:@selector(collectionView:didDeleteItemsAtIndexPaths:)]) {
        [self collectionView:self.collectionView didDeleteItemsAtIndexPaths:indexPaths];
      }
    };

    // Animate deletion.
    [self.collectionView performBatchUpdates:batchUpdates completion:completionBlock];
  }
}

- (void)deleteSections:(NSIndexSet *)sections {
  if ([self respondsToSelector:@selector(collectionView:willDeleteSections:)]) {
    void (^batchUpdates)(void) = ^{
      // Notify delegate to delete data.
      [self collectionView:self.collectionView willDeleteSections:sections];

      // Delete sections.
      [self.collectionView deleteSections:sections];
    };

    void (^completionBlock)(BOOL finished) = ^(__unused BOOL finished) {
      [self updateFooterInfoBarIfNecessary];
      // Notify delegate of deletion.
      if ([self respondsToSelector:@selector(collectionView:didDeleteSections:)]) {
        [self collectionView:self.collectionView didDeleteSections:sections];
      }
    };

    // Animate deletion.
    [self.collectionView performBatchUpdates:batchUpdates completion:completionBlock];
  }
}

- (void)updateHeaderInfoBarIfNecessary {
  if (_editor.isEditing) {
    // Show HUD only once before autodissmissing.
    BOOL allowsSwipeToDismissItem = NO;
    if ([self respondsToSelector:@selector(collectionViewAllowsSwipeToDismissItem:)]) {
      allowsSwipeToDismissItem = [self collectionViewAllowsSwipeToDismissItem:self.collectionView];
    }

    if (!_headerInfoBar.isVisible && !_headerInfoBarDismissed && allowsSwipeToDismissItem) {
      [_headerInfoBar showAnimated:YES];
    } else {
      [_headerInfoBar dismissAnimated:YES];
    }
  }
}

- (void)updateFooterInfoBarIfNecessary {
  NSInteger selectedItemCount = [self.collectionView.indexPathsForSelectedItems count];
  if (_editor.isEditing) {
    // Invalidate layout to add info bar if necessary.
    [self.collectionView.collectionViewLayout invalidateLayout];
    if (_footerInfoBar) {
      if (selectedItemCount > 0 && !_footerInfoBar.isVisible) {
        [_footerInfoBar showAnimated:YES];
      } else if (selectedItemCount == 0 && _footerInfoBar.isVisible) {
        [_footerInfoBar dismissAnimated:YES];
      }
    }
  } else if (selectedItemCount == 0 && _footerInfoBar.isVisible) {
    [_footerInfoBar dismissAnimated:YES];
  }
}

- (void)updateContentWithBottomInset:(CGFloat)inset {
  // Update bottom inset to account for footer info bar.
  UIEdgeInsets contentInset = self.collectionView.contentInset;
  contentInset.bottom += inset;
  [UIView animateWithDuration:MDCCollectionInfoBarAnimationDuration
                   animations:^{
                     self.collectionView.contentInset = contentInset;
                   }];
}

@end
